///////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2007-2009 ILOG, S.A.
//  All Rights Reserved.
//  The following is ILOG Source Code.  No modifications may be made to the  
//  ILOG Source Code.  Any usage of the ILOG Source Code is subject to 
//  the terms and conditions of the ILOG End User License Agreement   
//  applicable to this ILOG software product.
//
///////////////////////////////////////////////////////////////////////////////

package ilog.gauges.circular.renderers
{
  import flash.display.Graphics;
  import flash.geom.Point;
  import flash.geom.Rectangle;
  
  import ilog.gauges.gaugesClasses.circular.CircularScaleRelatedRenderer;
  import ilog.utils.CSSUtil;
  import ilog.utils.RectangleUtil;
  
  import mx.graphics.GradientEntry;
  import mx.graphics.IStroke;
  import mx.styles.CSSStyleDeclaration;

  /**
   * Specifies the stroke for the track.
   * This style must be an instance of an implementation of the the <code>IStroke</code> interface.
   * 
   * @default undefined 
   */
  [Style(name="stroke", type="mx.graphics.IStroke", inherit="no")]

  /**
   * Color of the track. This style property is only used if the <code>gradientEntries</code>
   * property is <code>null</code>, empty, or contains only one entry.
   *
   * @default 0xFF0000
   */
  [Style(name="rendererColor", type="uint", format="Color", inherit="no")]

  /**
   * Alpha of the track. This style property is only used if <code>gradientEntries</code>
   * is <code>null</code>, empty, or contains only one entry.
   *
   * @default 1.0
   */
  [Style(name="rendererAlpha", type="Number", inherit="no")]

  /**
   * A <code>CircularTrackRenderer</code> is used to represent a range of values on a scale.
   * The geometry of the track is determined by <code>startThickness</code> and 
   * <code>endThickness</code> which are expressed relative to the radius. If 
   * <code>startThickness</code> is equal to <code>endThickness</code>, the track 
   * has a constant thickness. However, if, for example, <code>endThickness</code> is 
   * greater than <code>startThickness</code>, the thickness of the track will 
   * increase when going from <code>minimum</code> to <code>maximum</code> value.  
   * The <code>placement</code> property controls whether the track is drawn 
   * inside, across, or outside of a circle defined by the origin and the radius.  
   * <p>
   * The track is filled with the <code>gradientEntries</code> array if it is 
   * defined. Otherwise, <code>rendererColor</code> and <code>rendererAlpha</code> style 
   * properties are used. 
   * </p>
   * 
   * @mxml
   *  
   * <p>The <code>&lt;ilog:CircularTrackRenderer&gt;</code> tag inherits all the properties
   * of its parent classes and adds the following properties:</p>
   *  
   * <pre>
   * &lt;ilog:CircularTrackRenderer
   *   <strong>Properties</strong>
   *   placement="inside|cross|outside" 
   *   startThickness="<i>0%</i>"
   *   endThickness="<i>20%</i>"
   *   gradientEntries="<i>No default</i>" 
   *    
   *   <strong>Styles</strong>
   *   rendererColor="0xFF0000"
   *   rendererAlpha="1.0"
   *   stroke="<i>No default</i>" 
   * /&gt;
   * </pre>
   *    
   * @see ilog.gauges.gaugesClasses.circular.CircularGaugeElement
   * @includeExample examples/CircularTrackRendererExample.mxml -noswf
   */
  public class CircularTrackRenderer extends CircularScaleRelatedRenderer
  {
    private static var stylesInited:Boolean = initStyles();
 
    private static function initStyles():Boolean
    {
      var trackStyle:CSSStyleDeclaration =
        CSSUtil.createSelector("CircularTrackRenderer");
        
      trackStyle.defaultFactory = function():void
        {
          this.rendererColor = 0xFF0000; 
          this.rendererAlpha = 1.0; 
        }

      return true;
    }

    /**
     *  Constructor
     */   
    public function CircularTrackRenderer():void
    {
      super();
    }

    /**
     *  @private 
     */   
    private var _minimum:Object;

    [Inspectable(category="Gauge")]  
        
    /**
     * The value at which the track starts. It serves to compute the start 
     * angle of the track by using the scale attached to the track.
     * 
     * @default null
     */
    public function get minimum():Object
    {
      return _minimum;
    }

    /**
     *  @private 
     */   
    public function set minimum(value:Object):void
    {
      _minimum = value;
      invalidateDisplayList();      
    }

    /**
     *  @private 
     */   
    private var _maximum:Object;

    [Inspectable(category="Gauge")]  

    /**
     * The value at which the track ends. It serves to compute the end 
     * angle of the track by using the scale attached to the track.
     * 
     * @default null
     */
    public function get maximum():Object
    {
      return _maximum;
    }

    /**
     *  @private 
     */ 
    public function set maximum(value:Object):void
    {
      _maximum = value;
      invalidateDisplayList();      
    }

    /**
     *  @private 
     */ 
    private var _gradientEntries:Array;
    
    [Inspectable(category="Style")]      

    /**
     * An array of <code>mx.graphics.GradientEntry</code> to fill the track. 
     * If this array is not specified, the style properties
     * <code>rendererColor</code> and <code>rendererAlpha</code>
     * are used.
     * 
     * @default null
     *      
     * @see mx.graphics.GradientEntry 
     */
    public function get gradientEntries():Array
    {
      return _gradientEntries;
    }

    /**
     *  @private 
     */     
    public function set gradientEntries(value:Array):void
    {
      _gradientEntries = value;
      invalidateDisplayList();      
    }

    /**
     *  @private 
     */ 
    private var _placement:String = "inside";

    [Inspectable(category="Gauge", enumeration="inside,outside,cross")]  

    /**
     * Specifies where to draw the track. Options are:
     * <ul>
     *   <li><code>"inside"</code> -
     *   Draws the track inside the radius defined by <code>radius</code>.</li>
     *
     *   <li><code>"outside"</code> -
     *   Draws the track outside the radius defined by <code>radius</code>.</li>
     *
     *   <li><code>"cross"</code> -
     *   Draws the track across the radius defined by <code>radius</code>.</li>
     * </ul>
     * 
     * @default "inside"
     */    
    public function get placement():String
    {
      return _placement;
    }

    /**
     *  @private 
     */ 
    public function set placement(value:String):void
    {
      _placement = value;
      invalidateDisplayList();
    }

    /**
     *  @private 
     */ 
    private function getStartThickness():Number
    {
      return isNaN(startThickness) ? getRadius() * percentStartThickness/100 : startThickness;
    }
    
    /**
     *  @private 
     */     
    private function getEndThickness():Number
    {
      return isNaN(endThickness) ? getRadius() * percentEndThickness/100 : endThickness;
    }

    /**
     *  @private 
     */ 
    private var _startThickness:Number = NaN;

    [Bindable("startThicknessChanged")]
    [Inspectable(category="Gauge")]
    [PercentProxy("percentStartThickness")]

    /**
     * Number that specifies the starting thickness of the track, in pixels. 
     *
     * <p><strong>Note:</strong> You can specify a percentage value in the MXML
     * <code>startThickness</code> attribute, such as <code>startThickness="0%"</code>,
     * but you cannot use a percentage value in the <code>startThickness</code>
     * property in ActionScript.
     * Use the <code>percentStartThickness</code> property instead.</p>
     * 
     * @default NaN
     */
    public function get startThickness():Number
    {
      return _startThickness;
    }

    /**
     *  @private 
     */ 
    public function set startThickness(value:Number):void
    {
      _startThickness = value;
      if (! isNaN(value))
        _percentStartThickness = NaN;
      invalidateDisplayList();      
    }
    
    /**
     *  @private 
     */         
    private var _percentStartThickness:Number = 0;

    [Inspectable(environment="none")]

    /**
     * Number that specifies the starting thickness of the track as a percentage
     * of <code>radius</code>.
     * 
     * Allowed values are 0-100.
     * 
     * @default 0
     */
    public function get percentStartThickness():Number
    {
      return _percentStartThickness;
    }

    /**
     *  @private 
     */ 
    public function set percentStartThickness(value:Number):void
    {
      _percentStartThickness = value;
      if (! isNaN(value))
        _startThickness = NaN;      
      invalidateDisplayList();
    }

    private var _endThickness:Number = NaN;

    [Bindable("endThicknessChanged")]
    [Inspectable(category="Gauge")]
    [PercentProxy("percentEndThickness")]

    /**
     * Number that specifies the ending thickness of the track, 
     * in pixels. 
     *
     * <p><strong>Note:</strong> You can specify a percentage value in the MXML
     * <code>endThickness</code> attribute, such as <code>endThickness="0%"</code>,
     * but you cannot use a percentage value in the <code>endThickness</code>
     * property in ActionScript.
     * Use the <code>percentEndThickness</code> property instead.</p>
     * 
     * @default NaN
     */
    public function get endThickness():Number
    {
      return _endThickness;
    }

    /**
     * @private 
     */ 
    public function set endThickness(value:Number):void
    {
      _endThickness = value;
      if (! isNaN(value))
        _percentEndThickness = NaN;      
      invalidateDisplayList();      
    }
        
    /**
     * @private 
     */         
    private var _percentEndThickness:Number = 20;

    [Inspectable(environment="none")]

    /**
     * Number that specifies the ending thickness of the track as a percentage
     * of <code>radius</code>.
     * Allowed values are 0-100.
     * 
     * @default 20
     */
    public function get percentEndThickness():Number
    {
      return _percentEndThickness;
    }

    /**
     *  @private 
     */ 
    public function set percentEndThickness(value:Number):void
    {
      _percentEndThickness = value;
      if (! isNaN(value))
        _endThickness = NaN;      
      invalidateDisplayList();
    }

    /**
     * Draws the track.
     *
     * @param unscaledWidth Specifies the width, in pixels, of the component
     * in the component's coordinates, regardless of the value of the
     * <code>scaleX</code> property of the component.
     *
     * @param unscaledHeight Specifies the height, in pixels, of the component
     * in the component's coordinates, regardless of the value of the
     * <code>scaleY</code> property of the component. 
     */ 
    override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
    {
      super.updateDisplayList(unscaledWidth, unscaledHeight);      
      if (!scale)
        return;
      var o:Point = getOrigin();
      var r:Number = getRadius();
      var sw:Number = getStartThickness();
      var ew:Number = getEndThickness();
      
      var g:Graphics = graphics;
      var startAngleRadians:Number = scale.angleForValue(minimum);
      var endAngleRadians:Number = scale.angleForValue(maximum)

        if (gradientEntries != null && gradientEntries.length > 1)
          drawGradientTrack(g, o.x, o.y, r, scale.orientation, placement, startAngleRadians, endAngleRadians, sw, ew, gradientEntries);
        else
          drawColorTrack(g, o.x, o.y, r, scale.orientation, placement, startAngleRadians, endAngleRadians, sw, ew, getStyle("rendererColor"), getStyle("rendererAlpha"));
    }
 
    /**
     *  @private 
     */  
    private function colorInterpolation(c1:uint, c2:uint, len:int):Array
    {
      var ret:Array = [];
      for (var i:int = 0; i < len; i++)
        ret.push(interpolateColor(c1, c2, i / (len - 1)));
      return ret;
    }

    /**
     *  @private 
     */ 
    private static function interpolateColor(from:uint, dest:uint, n:Number):uint
    {      
      var fr:Number = (from >> 16) & 0xff;
      var fg:Number = (from >> 8) & 0xff;
      var fb:Number = from & 0xff;
    
      var tr:Number = (dest >> 16) & 0xff;
      var tg:Number = (dest >> 8) & 0xff;
      var tb:Number = dest & 0xff;
  
      var r:int = ((1 - n) * fr  + n * tr) & 0xff;
      var g:int = ((1 - n) * fg  + n * tg) & 0xff;
      var b:int = ((1 - n) * fb  + n * tb) & 0xff;
    
      return r << 16 | g << 8 | b;
    }
      
    /**
     *  @private 
     */ 
    private function colorsInterpolation(colors:Array, ratios:Array, len:int):Array
    {
      var ret:Array = [];
      var ilen:Number = 0;

      for (var i:int = 0; i < colors.length - 1; i++)
      {
        ilen = (ratios[i+1] - ratios[i]) * len;
        ilen = Math.round(ilen);
        ret = ret.concat(colorInterpolation(colors[i], colors[i+1], ilen));
      }
      
      return ret;
    }

    /**
     *  @private 
     */ 
    private function alphaInterpolation(c1:Number, c2:Number, len:int):Array
    {
      var step:Number = (c2 - c1) / (len - 1) ;
      var ret:Array = [];
      var j:int = 0;
      for (var i:int = 0; i < len; i++)
      {
        ret.push(c1 + i * step);
      }
      
      return ret;
    }

    /**
     *  @private 
     */     
    private function alphasInterpolation(alphas:Array, positions:Array, len:int):Array
    {
      var ret:Array = [];
      var ilen:Number = 0;
      for (var i:int = 0; i < alphas.length - 1; i++)
      {
        ilen = (positions[i+1] - positions[i]) * len;
        ilen = Math.round(ilen);
        ret = ret.concat(alphaInterpolation(alphas[i], alphas[i+1], ilen));
      }
      return ret;
    }

    /**
     *  @private 
     */ 
    private function getEntriesFor(entries:Array, attr:String):Array
    {
      var ret:Array = [];
      for each (var e:GradientEntry in entries)
        {
          ret.push(e[attr]);
        }
      return ret;
    }

    /**
     *  @private 
     */  
    private function distance(x1:Number, y1:Number, x2:Number, y2:Number):Number
    {
      return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
    }

    /**
     *  @private 
     */ 
    private function drawColorTrack(g:Graphics, ox:Number, oy:Number, 
                                    radius:Number, orientation:String,
                                    placement:String,  
                                    startAngleRadians:Number, endAngleRadians:Number, 
                                    sWeight:Number, eWeight:Number, 
                                    color:uint, alpha:Number):void
    {
      var angleStep:Number = Math.PI / 360;
      var totalAngle:Number;
      totalAngle = computeAngle(startAngleRadians, endAngleRadians, orientation);

      var iterCount:Number = int(totalAngle / angleStep);      
      var innerRadius:Number;
      var outerRadius:Number;
      var outerStep:Number = 0;
      var innerStep:Number = 0;
      var px:Number;
      var py:Number;

      switch (placement)
      {
      case "inside": 
        innerRadius = - sWeight ; 
        outerRadius = 0; 
        innerStep = (sWeight - eWeight) / iterCount;
        break;
      case "outside": 
        innerRadius = 0; 
        outerRadius = sWeight; 
        outerStep =  -(sWeight - eWeight) / iterCount;
        break;
      case "cross": 
        innerRadius = -sWeight/2; 
        outerRadius = sWeight/2; 
        outerStep = -(sWeight - eWeight) / iterCount / 2;
        innerStep = (sWeight - eWeight) / iterCount / 2;          
        break;
      }

      var angle:Number;
      var i:int;
      if (orientation == "clockwise")
        angleStep = -angleStep;

      graphics.clear();
            
      var stroke:IStroke = getStyle("stroke");
      if (stroke)
        stroke.apply(g);  

      g.beginFill(color, alpha);

      px = ox + Math.cos(startAngleRadians) * (radius + innerRadius);
      py = oy - Math.sin(startAngleRadians) * (radius + innerRadius);
      g.moveTo(px, py);
      for (i = 1; i < iterCount; i++)
      {
        angle = startAngleRadians + i * angleStep;
        px = ox + Math.cos(angle + angleStep) * (radius + innerRadius + i * innerStep);
        py = oy - Math.sin(angle + angleStep) * (radius + innerRadius + i * innerStep);
        g.lineTo(px, py);
      }
      if (isNaN(angle))
        angle = startAngleRadians;
      px = ox + Math.cos(angle + angleStep) * (radius + outerRadius + (iterCount-1) * outerStep);
      py = oy - Math.sin(angle + angleStep) * (radius + outerRadius + (iterCount-1) * outerStep);
      g.lineTo(px, py);
      for (i = iterCount-1; i >= -1; i--)
      {
        angle = startAngleRadians + i * angleStep;
        px = ox + Math.cos(angle + angleStep) * (radius + outerRadius + i * outerStep);
        py = oy - Math.sin(angle + angleStep) * (radius + outerRadius + i * outerStep);
        g.lineTo(px, py);
      }
      px = ox + Math.cos(startAngleRadians) * (radius + innerRadius);
      py = oy - Math.sin(startAngleRadians) * (radius + innerRadius);
      g.lineTo(px, py);
    }

    /**
     *  @private 
     */     
    private function drawGradientTrack(g:Graphics, ox:Number, oy:Number, 
                                       radius:Number,  orientation:String,
                                       placement:String, 
                                       startAngleRadians:Number, endAngleRadians:Number, 
                                       sWeight:Number, eWeight:Number, 
                                       entries:Array):void
    {

      var p0:Point = new Point();
      var p1:Point = new Point();
      var p2:Point = new Point();
      var p3:Point = new Point();

      var angleStep:Number = Math.PI / 360;
      var cidx:uint = 0;
      var totalAngle:Number;
      var ic:Array;
      var ia:Array;
      var colors:Array;
      var colorsRatios:Array;
      var alphas:Array;
      totalAngle = computeAngle(startAngleRadians, endAngleRadians, orientation);

      colors = getEntriesFor(entries, "color");
      colorsRatios = getEntriesFor(entries, "ratio");
      alphas = getEntriesFor(entries, "alpha");

      var iterCount:Number = int(totalAngle / angleStep);      
      
      ic = colorsInterpolation(colors, colorsRatios, iterCount);
      ia = alphasInterpolation(alphas, colorsRatios, iterCount);

      var r0:Number;
      var r1:Number;
      var r2Step:Number = 0;
      var r3Step:Number = 0;

      switch (placement)
      {
      case "inside": 
        r0 = - sWeight ; 
        r1 = 0; 
        r3Step = (sWeight - eWeight) / iterCount;
        break;
      case "outside": 
        r0 = 0; 
        r1 = sWeight; 
        r2Step =  -(sWeight - eWeight) / iterCount;
        break;
      case "cross": 
        r0 = -sWeight/2; 
        r1 =sWeight/2; 
        r2Step = -(sWeight - eWeight) / iterCount / 2;
        r3Step = (sWeight - eWeight) / iterCount / 2;          
        break;
      }
      
      p0.x = ox + Math.cos(startAngleRadians) * (radius + r0);
      p0.y = oy - Math.sin(startAngleRadians) * (radius + r0);
      p1.x = ox + Math.cos(startAngleRadians) * (radius + r1);
      p1.y = oy - Math.sin(startAngleRadians) * (radius + r1);
      
      graphics.clear();

      var angle:Number;
      if (orientation == "clockwise")
        angleStep = -angleStep;
      
      var innerXPts:Array = [];
      var innerYPts:Array = [];
      var outerXPts:Array = [];
      var outerYPts:Array = [];
      
      for (var i:Number = 0; i < iterCount; i++)
      {
        angle = startAngleRadians + i * angleStep;
   
        p2.x = ox + Math.cos(angle + angleStep) * (radius + r1 + i * r2Step);
        p2.y = oy - Math.sin(angle + angleStep) * (radius + r1 + i * r2Step);
        p3.x = ox + Math.cos(angle + angleStep) * (radius + r0 + i * r3Step);
        p3.y = oy - Math.sin(angle + angleStep) * (radius + r0 + i * r3Step);

        g.beginFill(ic[cidx],ia[cidx]);
        cidx++;
        
        g.moveTo(p0.x, p0.y);
        g.lineTo(p1.x, p1.y);

        g.lineTo(p2.x, p2.y);
        g.lineTo(p3.x, p3.y);

        g.lineTo(p0.x, p0.y);
        g.endFill();

        innerXPts.push(p0.x);
        innerYPts.push(p0.y);
        outerXPts.push(p1.x);
        outerYPts.push(p1.y);

        p0.x = p3.x;
        p0.y = p3.y;
        p1.x = p2.x;
        p1.y = p2.y;
        
      }
      
      innerXPts.push(p0.x);
      innerYPts.push(p0.y);
      outerXPts.push(p1.x);
      outerYPts.push(p1.y);


      var stroke:IStroke = getStyle("stroke");
      if (! stroke)
        return;
      else
        stroke.apply(g);  

      g.moveTo(innerXPts[0], innerYPts[0]);
      
      for (i = 1; i < innerXPts.length; i++)
        g.lineTo(innerXPts[i], innerYPts[i]);
      
      g.moveTo(outerXPts[0], outerYPts[0]);
      
      for (i = 1; i < outerXPts.length; i++)
        g.lineTo(outerXPts[i], outerYPts[i]);
      
      g.moveTo(innerXPts[0], innerYPts[0]);
      g.lineTo(outerXPts[0], outerYPts[0]);

      g.moveTo(innerXPts[innerXPts.length - 1], innerYPts[innerYPts.length - 1]);
      g.lineTo(outerXPts[outerXPts.length - 1], outerYPts[outerYPts.length - 1]);
    }

    /**
     *  @private 
     */     
    private function measureTrack():Rectangle
    {
      var pts:Array = [];
      var p0:Point = new Point();
      var p1:Point = new Point();
      var p2:Point = new Point();
      var p3:Point = new Point();

      var angleStep:Number = Math.PI / 360;
      var totalAngle:Number;
      var alphas:Array;
      var startAngleRadians:Number = scale.angleForValue(minimum);
      var endAngleRadians:Number = scale.angleForValue(maximum);
      
      totalAngle = computeAngle(startAngleRadians, endAngleRadians, scale.orientation);

      var iterCount:Number = int(totalAngle / angleStep);      

      var r0:Number;
      var r1:Number;
      var r2Step:Number = 0;
      var r3Step:Number = 0;
      var sWeight:Number = startThickness;
      var eWeight:Number = endThickness;
      var ox:Number = originX;
      var oy:Number = originY;
            
      switch (placement)
      {
      case "inside": 
        r0 = - sWeight ; 
        r1 = 0; 
        r3Step = (sWeight - eWeight) / iterCount;
        break;
      case "outside": 
        r0 = 0; 
        r1 = sWeight; 
        r2Step =  -(sWeight - eWeight) / iterCount;
        break;
      case "cross": 
        r0 = -sWeight/2; 
        r1 =sWeight/2; 
        r2Step = -(sWeight - eWeight) / iterCount / 2;
        r3Step = (sWeight - eWeight) / iterCount / 2;          
        break;
      }
      
      p0.x = ox + Math.cos(startAngleRadians) * (radius + r0);
      p0.y = oy - Math.sin(startAngleRadians) * (radius + r0);
      p1.x = ox + Math.cos(startAngleRadians) * (radius + r1);
      p1.y = oy - Math.sin(startAngleRadians) * (radius + r1);

      var angle:Number;
      if (scale.orientation == "clockwise")
        angleStep = -angleStep;
      
      for (var i:Number = 0; i < iterCount; i++)
      {
        angle = startAngleRadians + i * angleStep;
   
        p2.x = ox + Math.cos(angle + angleStep) * (radius + r1 + i * r2Step);
        p2.y = oy - Math.sin(angle + angleStep) * (radius + r1 + i * r2Step);
        p3.x = ox + Math.cos(angle + angleStep) * (radius + r0 + i * r3Step);
        p3.y = oy - Math.sin(angle + angleStep) * (radius + r0 + i * r3Step);


        pts.push(p0.clone(), p1.clone(), p2.clone(), p3.clone());

        p0.x = p3.x;
        p0.y = p3.y;
        p1.x = p2.x;
        p1.y = p2.y;
        
      }
      return RectangleUtil.bbox(pts);
    }
    
    /**
     *  @private
     */
    override protected function measure():void
    {
      super.measure();
      if (measuredWidth == 0 ||
          isNaN(startThickness) ||
          isNaN(endThickness))
      {
        measuredMinWidth = measuredMinHeight = measuredWidth = measuredHeight = 0;        
      }
      else
      {
        var bb:Rectangle = measureTrack();
        measuredMinWidth = measuredWidth = bb.x + bb.width;
        measuredMinHeight = measuredHeight = bb.y + bb.height;
      }
    }    
  }
}
